﻿using System.Collections.Generic;
using System.Linq;
using JetBlack.AuthFeedBus.Messages;
using JetBlack.Diagnostics;
using JetBlack.Text;

namespace JetBlack.AuthFeedBus.Distributor
{
    /// <summary>
    /// This class handles notification messages.
    /// As a holder of the notifications it is also called upon to provide the
    /// feed names and notification services of notifiers which remove notifications
    /// either intentionally or by closing their connection.
    /// </summary>
    internal class NotificationManager
    {
        internal delegate void OnNotificationResponseHandler(Interactor notifiable, IEnumerable<string> feeds, bool isPublisher, bool isAdd);

        /// <summary>
        /// This event gets called when a new notification request is made.
        /// This allows existing subscriptions to be checked. Where the
        /// topics match the subscriptions are forwarded to the
        /// notifiable interactor.
        /// </summary>
        internal event OnNotificationResponseHandler OnNotificationResponse;

        /// <summary>
        /// This class encapsulates a notiable interactor and the service it supports
        /// through the notification.
        /// </summary>
        private class NotifiableAndService : IEqualityComparer<NotifiableAndService>
        {
            public readonly Interactor Notifiable;
            public readonly bool IsPublisher;

            public NotifiableAndService(Interactor notifiable, bool isPublisher)
            {
                Notifiable = notifiable;
                IsPublisher = isPublisher;
            }

            public bool Equals(NotifiableAndService x, NotifiableAndService y)
            {
                return (x == null && y == null) || (x != null && y != null && x.Notifiable == y.Notifiable && x.Notifiable == y.Notifiable);
            }

            public int GetHashCode(NotifiableAndService obj)
            {
                return Notifiable.GetHashCode() ^ IsPublisher.GetHashCode();
            }
        }

        private readonly Dictionary<string, List<NotifiableAndService>> cache = new Dictionary<string, List<NotifiableAndService>>();

        /// <summary>
        /// This method handles a request by a client to be notified when a
        /// subscription is made.
        /// </summary>
        /// <param name="notifyMessage">The notify message received from the client.</param>
        /// <param name="notifiable">The notifiable interactor.</param>
        internal void HandleRequest(NotificationRequest notifyMessage, Interactor notifiable)
        {
            AppEnvironment.TraceSource.Verbose("Handling notification request for {0} on {1}", notifiable, notifyMessage);

            NotifiableAndService notifiableAndService = new NotifiableAndService(notifiable, notifyMessage.IsPublisher);

            bool sendNotification;
            if (notifyMessage.IsAdd)
                sendNotification = AddNotification(notifiableAndService, notifyMessage.Feed);
            else
                sendNotification = RemoveNotification(notifiableAndService, notifyMessage.Feed);

            if (sendNotification && OnNotificationResponse != null)
                OnNotificationResponse(notifiable, new[] { notifyMessage.Feed }, notifyMessage.IsPublisher, notifyMessage.IsAdd);
        }

        /// <summary>
        /// Add a new notification.
        /// </summary>
        /// <param name="notifiableAndService">The notifiable interactor and the service it supports.</param>
        /// <param name="feed">The feed name.</param>
        /// <returns>Returns true if this was a new notification.</returns>
        private bool AddNotification(NotifiableAndService notifiableAndService, string feed)
        {
            if (!cache.ContainsKey(feed))
                cache.Add(feed, new List<NotifiableAndService>());

            if (cache[feed].Contains(notifiableAndService))
                return false;
            else
            {
                cache[feed].Add(notifiableAndService);
                return true;
            }
        }

        /// <summary>
        /// Removes a notification.
        /// </summary>
        /// <param name="notifiableAndService">The notifiable interactor and the service it supports.</param>
        /// <param name="feed">The feed name.</param>
        /// <returns>Returns true if this was the last notiable that was a publisher.</returns>
        private bool RemoveNotification(NotifiableAndService notifiableAndService, string feed)
        {
            if (cache.ContainsKey(feed) && cache[feed].Contains(notifiableAndService))
            {
                cache[feed].Remove(notifiableAndService);

                int serviceCount = cache[feed].Count(value => value.IsPublisher == notifiableAndService.IsPublisher);

                if (cache[feed].Count == 0)
                    cache.Remove(feed);

                return serviceCount == 0 && notifiableAndService.IsPublisher;
            }

            return false;
        }

        /// <summary>
        /// This method removes all associations between notifications and this
        /// interactor. This is required when a client disconnects.
        /// </summary>
        /// <param name="notifiable">The notifiable interactor</param>
        internal void RemoveInteractor(Interactor notifiable)
        {
            AppEnvironment.TraceSource.Verbose("Removing notification requests from {0}", notifiable);

            // Identify the notifications this interactor has registered
            Dictionary<bool, List<string>> releventNotifications = new Dictionary<bool, List<string>>();
            foreach (KeyValuePair<string, List<NotifiableAndService>> cacheItem in cache)
            {
                foreach (NotifiableAndService notifiableAndService in cacheItem.Value)
                {
                    if (notifiableAndService.Notifiable == notifiable)
                    {
                        if (!releventNotifications.ContainsKey(notifiableAndService.IsPublisher))
                            releventNotifications.Add(notifiableAndService.IsPublisher, new List<string>());
                        if (!releventNotifications[notifiableAndService.IsPublisher].Contains(cacheItem.Key))
                            releventNotifications[notifiableAndService.IsPublisher].Add(cacheItem.Key);
                    }
                }
            }

            // Remove those notifications from the cache
            foreach (KeyValuePair<bool, List<string>> releventNotification in releventNotifications)
            {
                NotifiableAndService notifiableAndService = new NotifiableAndService(notifiable, releventNotification.Key);

                foreach (string feed in releventNotification.Value)
                {
                    cache[feed].Remove(notifiableAndService);
                    if (cache[feed].Count == 0)
                        cache.Remove(feed);
                }
            }

            if (releventNotifications.Count > 0 && OnNotificationResponse != null)
            {
                foreach (KeyValuePair<bool, List<string>> releventNotification in releventNotifications)
                    OnNotificationResponse(notifiable, releventNotification.Value, releventNotification.Key, false);
            }
        }

        internal void NotifyListeners(ForwardedSubscriptionRequest message)
        {
            List<Interactor> interactors = new List<Interactor>();
            if (cache.ContainsKey(message.Feed))
            {
                AppEnvironment.TraceSource.Verbose("Notifying interactors[{0}] of subscription {1}", cache[message.Feed].ToDelimitedString(", ", x => x.ToString()), message);

                foreach (NotifiableAndService notifiableAndService in cache[message.Feed])
                    notifiableAndService.Notifiable.SendMessage(message);
            }
        }
    }
}
